iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 22
1
Mobile Development

程式初學:Android與Kotlin系列 第 22

Day 22--天氣app(八)觀察者模式,匿名函數,lambda表示式

  • 分享至 

  • xImage
  •  

在此專案,MainActivity取得的天氣資料是被觀察的目標
而二個顯示的fragment畫面就是觀察者

目標interface

一樣是建立包含3種方法的interface

  • 更新有訂閱的觀察者
  • 讓觀察者訂閱
  • 讓觀察者取消訂閱
interface ILocationPublisher {
    fun getCurrentLocationWeatherRecord(): WeatherData.Records.Location?
    fun add(subscriber: () -> Unit)
    fun remove(subscriber: () -> Unit)
}

interface裡的add/remove函數使用另外一種寫法
參數subscriber是() -> Unit型態,表示須要傳入的是一個沒有回傳值的匿名函數
而這個匿名函數定義在各觀察者中

lambda

lambda是讓一段程式碼可以像物件一樣傳遞,例如:
下方的subscriber變數就是指向以lambda寫的一段程式物件

class SecondFragment : Fragment() {
    ...
    private val subscriber: () -> Unit = {
        val loc = (activity as ILocationPublisher).getCurrentLocationWeatherRecord()

        loc?.let {
            updateContent(it)
        }
    }

    fun updateContent(location: WeatherData.Records.Location) {
        //隔日
        tv_startTime3.text = location.weatherElement[0].time[2].startTime
        tv_endTime3.text =
            location.weatherElement[0].time[2].endTime//.split(" ")[1]
        tv_status3.text =
            location.weatherElement[0].time[2].parameter.parameterName
        tv_rainProbability3.text =
            "降雨機率 ${location.weatherElement[1].time[2].parameter.parameterName} %"
    }
}

函數型態(funtion type)

這裡表示變數subscriber存放的就是() -> Unit型態的函數
當呼叫ILocationPublisher.add時,就可以把subscriber傳遞過去

(activity as ILocationPublisher).add(subscriber)

其實如果不用匿名函數的話

private val subscriber: () -> Unit = {
        val loc = (activity as ILocationPublisher).getCurrentLocationWeatherRecord()

        loc?.let {
            updateContent(it)
        }
    }

以上可以寫成

fun subscriber(){
        val loc = (activity as ILocationPublisher).getCurrentLocationWeatherRecord()

        loc?.let {
            updateContent(it)
        }
    }

但此時這個subscriber就不是一個變數,而是函數名
當使用它傳遞,是傳遞函數的回傳值(Unit),而不是整個函數

可以看到編譯器提示型態錯誤了,所以還是須要用lambda語法

(activity as ILocationPublisher).add { subscriber() }

如此就是將整個subscriber()函數作爲引數傳入add了

回到觀察者模式

MainActivity實作interface ILocationPublisher

class MainActivity : AppCompatActivity(), ILocationPublisher {
    ...

    private var location: WeatherData.Records.Location? = null
    private val locationSubscribers = mutableListOf<() -> Unit>()

    private fun setQueryResult(location: WeatherData.Records.Location) {
        this.location = location
        locationSubscribers.forEach { it.invoke() }
    }

    override fun getCurrentLocationWeatherRecord(): WeatherData.Records.Location? {
        return this.location
    }

    override fun add(subscriber: () -> Unit) {
        locationSubscribers.add(subscriber)
    }

    override fun remove(subscriber: () -> Unit) {
        locationSubscribers.remove(subscriber)
    }
}

在搜尋後呼叫並將資料傳入setQueryResult

override fun onCreateOptionsMenu(menu: Menu?): Boolean {
        menuInflater.inflate(R.menu.menu, menu)
        val searchItem = menu?.findItem(R.id.action_search)
        if (searchItem != null) {
            val searchView = searchItem.actionView as SearchView
            searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
                override fun onQueryTextSubmit(query: String?): Boolean {
                    return true
                }

                override fun onQueryTextChange(newText: String?): Boolean {
                    if (newText!!.isNotEmpty()) {
                        weatherList.forEach { location ->
                            if (location.locationName.contains(newText)) {
                                setQueryResult(location)
                            }
                        }
                    }
                    return true
                }
            })
        }
        return super.onCreateOptionsMenu(menu)
    }

二個fragment(因爲拿來練習的天氣資料只有預報36小時,所以改成二頁)
加入上面lambda的subscriber與updateContent()以訂閱並更新各自的畫面

要注意的是fragment的生命週期,當載入時訂閱沒有問題
但當destroy後,要記得取消訂閱,否則fragment沒有東西可更新時,卻還是收到通知
就會出錯

class SecondFragment : Fragment() {
    ...
    override fun onDestroyView() {
        super.onDestroyView()
        (activity as ILocationPublisher).remove(subscriber)    //取消訂閱
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        (activity as ILocationPublisher).add(subscriber)    //訂閱
    }
}    

把需要的資料都整理一下,畫面大致如下

有發現一個問題:查詢後,今日的畫面有顯示資料,但明日預報沒有?

原來因爲當app開啓時,只有今日的fragment先被載入
明日預報尚未載入,要載入後觸發在onActivityCreated()內的訂閱才會執行更新

所以當畫面滑過去明日預報的fragment後,需要再次查詢,畫面才會獲得更新

Fragment preload

解決方法就是使用viewPager2.offscreenPageLimit
讓非當前畫面的Second Fragment預先載入
這樣在Second Fragment的subscribe才會在app啓動時就生效

viewPager2.offscreenPageLimit = 1

app使用起來也比較正常了

https://stackoverflow.com/questions/55573812/preload-next-page-in-viewholder2-like-it-was-automatically-done-in-viewpager


上一篇
Day 21--天氣app(七)觀察者模式 Observer Pattern
下一篇
Day 23--天氣app(九)取得所在位置 part 1
系列文
程式初學:Android與Kotlin30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言